﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using SSTU.Contract.Facade;
using SSTU.Contract.Common;
using SSTU.Model;
using System.Threading;
using SSTU.Management.Dependency;
using SSTU.Data;
using System.Linq;
using System.Collections.Generic;

namespace SSTU.Facade
{
	public class CacheTimeTableFacade : FacadeBase, ITimeTableFacade
	{
		private readonly ITimeTableFacade _innerTimeTabeFacade;
		private readonly object TimeTableSyncRoot = new object();

		/// <summary>
		/// Initializes a new instance of the <see cref="CacheTimeTableFacade"/> class.
		/// </summary>
		/// <param name="innerTimeTableFacade">The inner time table facade.</param>
		public CacheTimeTableFacade(ITimeTableFacade innerTimeTableFacade)
		{
			DependencyRequirement.Current.NotNullFromConstructorInjection<CacheTimeTableFacade, ITimeTableFacade>(innerTimeTableFacade);
			_innerTimeTabeFacade = innerTimeTableFacade;
		}

		#region ITimeTableFacade

		/// <summary>
		/// Loads the time table for group asynchronously and invoke callback.
		/// </summary>
		/// <param name="groupId">The group identifier.</param>
		/// <param name="asyncCallback">The async callback.</param>
		public void LoadTimeTableForGroupAsync(string groupId, FacadeAsyncCallback<GroupTimeTable> asyncCallback)
		{
			var syncContext = SynchronizationContext.Current;
			_innerTimeTabeFacade.LoadTimeTableForGroupAsync(groupId, innerFacadeResponse =>
			{
				bool isSuccess =
					innerFacadeResponse != null
					&& innerFacadeResponse.IsSuccess
					&& innerFacadeResponse.Data != null;
				if (isSuccess)
				{
					ThreadPool.QueueUserWorkItem(o => SaveTimeTableForGroupInCache(groupId, innerFacadeResponse.Data));
				}
				asyncCallback(innerFacadeResponse);
			});
			DoAsync(FacadeResponseDataSource.Local, () => LoadTimeTableForGroupFromCache(groupId), asyncCallback);
		}

		#endregion // ITimeTableFacade

		#region Help Methods: Time Table

		/// <summary>
		/// Saves the time table for group in cache.
		/// </summary>
		/// <param name="groupId">The group id.</param>
		/// <param name="groupTimeTable">The group time table.</param>
		private void SaveTimeTableForGroupInCache(string groupId, GroupTimeTable groupTimeTable)
		{
			lock (TimeTableSyncRoot)
			{
				using (var dataContext = SstuDataContext.Create())
				{
					// In each caching procedure we removes all existing mapping for the current group.
					var existingMap = dataContext.GroupToClassMap.Where(m => m.GroupId == groupId);
					dataContext.GroupToClassMap.DeleteAllOnSubmit(existingMap);
					dataContext.SubmitChanges();
					if (groupTimeTable.EvenWeek != null)
					{
						SavePeriodicWeekInDataContext(dataContext, groupId, groupTimeTable.EvenWeek);
					}
					if (groupTimeTable.OddWeek != null)
					{
						SavePeriodicWeekInDataContext(dataContext, groupId, groupTimeTable.OddWeek);
					}
					dataContext.SubmitChanges();
				}
			}
		}

		/// <summary>
		/// Loads the time table for group from cache.
		/// </summary>
		/// <param name="groupId">The group identifier.</param>
		private GroupTimeTable LoadTimeTableForGroupFromCache(string groupId)
		{
			GroupTimeTable groupTimeTable = null;
			groupTimeTable = new GroupTimeTable();
			groupTimeTable.Group = new Group(groupId);
			ClassEntity[] groupClasses = null;
			lock (TimeTableSyncRoot)
			{
				using (var dataContext = SstuDataContext.Create())
				{
					var groupClassesIds = dataContext.GroupToClassMap.Where(mapEntity => mapEntity.GroupId == groupId).Select(map => map.ClassId).ToArray();
					groupClasses = dataContext.Classes.Where(classEntity => groupClassesIds.Contains(classEntity.Id)).ToArray();
				}
			}
			var evenWeekClasses = groupClasses.Where(classEntity => classEntity.Week == (int)PeriodicWeekType.Even);
			groupTimeTable.EvenWeek = new PeriodicWeek(PeriodicWeekType.Even, ConvertToDays(evenWeekClasses));
			var oddWeekClasses = groupClasses.Where(classEntity => classEntity.Week == (int)PeriodicWeekType.Odd);
			groupTimeTable.OddWeek = new PeriodicWeek(PeriodicWeekType.Odd, ConvertToDays(oddWeekClasses));
			return groupTimeTable;
		}

		/// <summary>
		/// Converts classes entities to periodic week days.
		/// </summary>
		/// <param name="classes">The classes entities.</param>
		private IEnumerable<PeriodicWeekDay> ConvertToDays(IEnumerable<ClassEntity> classes)
		{
			if (classes == null)
			{
				return Enumerable.Empty<PeriodicWeekDay>();
			}
			var days = new List<PeriodicWeekDay>()
			{
				new PeriodicWeekDay(DayOfWeek.Monday, true),
				new PeriodicWeekDay(DayOfWeek.Tuesday, true),
				new PeriodicWeekDay(DayOfWeek.Wednesday, true),
				new PeriodicWeekDay(DayOfWeek.Thursday, true),
				new PeriodicWeekDay(DayOfWeek.Friday, true),
				new PeriodicWeekDay(DayOfWeek.Saturday, true),
				new PeriodicWeekDay(DayOfWeek.Sunday, true),
			};
			foreach (var day in days)
			{
				var dayClasses = classes.Where(classEntity => classEntity.Day == (int)day.DayOfWeek);
				foreach (var dayClassEntity in dayClasses)
				{
					var dayClass = new Class()
					{
						Id = dayClassEntity.Id,
						Start = dayClassEntity.StartTicks.HasValue ? TimeSpan.FromTicks(dayClassEntity.StartTicks.Value) : (TimeSpan?)null,
						End = dayClassEntity.EndTicks.HasValue ? TimeSpan.FromTicks(dayClassEntity.EndTicks.Value) : (TimeSpan?)null,
						RoomId = dayClassEntity.RoomId,
						SubRoomId = dayClassEntity.SubRoomId,
						BuildingOrOrganizationId = dayClassEntity.BuildingOrOrganizationId,
						Lecturer = new User() { Id = dayClassEntity.LecturerUserId, FullName = dayClassEntity.LecturerUserFullName, },
						Theme = dayClassEntity.Theme
					};
					day.Classes.Add(dayClass);
				}
			}
			return days;
		}

		/// <summary>
		/// Updates the periodic week in data context.
		/// </summary>
		/// <param name="dataContext">The dc.</param>
		/// <param name="groupId">The group id.</param>
		/// <param name="week">The week.</param>
		private void SavePeriodicWeekInDataContext(SstuDataContext dataContext, string groupId, PeriodicWeek week)
		{
			if (week.Days != null)
			{
				foreach (var newDay in week.Days)
				{
					foreach (var newClass in newDay.Classes.Where(c => !string.IsNullOrEmpty(c.Id)))
					{
						var existingClassEntity = dataContext.Classes.FirstOrDefault(pc => pc.Id == newClass.Id);
						if (existingClassEntity == null)
						{
							var newClassEntity = new ClassEntity()
							{
								Id = newClass.Id,
								StartTicks = newClass.Start.HasValue ? newClass.Start.Value.Ticks : (long?)null,
								EndTicks = newClass.End.HasValue ? newClass.End.Value.Ticks : (long?)null,
								RoomId = newClass.RoomId,
								SubRoomId = newClass.SubRoomId,
								BuildingOrOrganizationId = newClass.BuildingOrOrganizationId,
								LecturerUserId = newClass.Lecturer == null ? null : newClass.Lecturer.Id,
								LecturerUserFullName = newClass.Lecturer == null ? null : newClass.Lecturer.FullName,
								Day = (int)newDay.DayOfWeek,
								Week = (int)week.Type,
								Theme = newClass.Theme,
							};
							dataContext.Classes.InsertOnSubmit(newClassEntity);
						}
						else
						{
							existingClassEntity.Id = newClass.Id;
							existingClassEntity.StartTicks = newClass.Start.HasValue ? newClass.Start.Value.Ticks : (long?)null;
							existingClassEntity.EndTicks = newClass.End.HasValue ? newClass.End.Value.Ticks : (long?)null;
							existingClassEntity.RoomId = newClass.RoomId;
							existingClassEntity.SubRoomId = newClass.SubRoomId;
							existingClassEntity.BuildingOrOrganizationId = newClass.BuildingOrOrganizationId;
							existingClassEntity.LecturerUserId = newClass.Lecturer == null ? null : newClass.Lecturer.Id;
							existingClassEntity.LecturerUserFullName = newClass.Lecturer == null ? null : newClass.Lecturer.FullName;
							existingClassEntity.Day = (int)newDay.DayOfWeek;
							existingClassEntity.Week = (int)week.Type;
							existingClassEntity.Theme = newClass.Theme;
						}
						if (dataContext.GroupToClassMap.FirstOrDefault(m => (m.GroupId == groupId) && (m.ClassId == newClass.Id)) == null)
						{
							var newMapEntity = new GroupToClassMapEntity() { ClassId = newClass.Id, GroupId = groupId };
							dataContext.GroupToClassMap.InsertOnSubmit(newMapEntity);
						}
					}
				}
			}
		}

		#endregion // Help Methods: Time Table
	}
}
